import { addClass, elt, rmClass } from '../util/dom.js';
import { on } from '../util/event.js';
import { scrollGap, paddingVert } from '../measurement/position_measurement.js';
import { ie, ie_version, mac, mac_geMountainLion } from '../util/browser.js';
import { updateHeightsInViewport } from './update_lines.js';
import { Delayed } from '../util/misc.js';

import { setScrollLeft, updateScrollTop } from './scrolling.js';

// SCROLLBARS

// Prepare DOM reads needed to update the scrollbars. Done in one
// shot to minimize update/measure roundtrips.
export function measureForScrollbars(cm) {
  let d = cm.display,
    gutterW = d.gutters.offsetWidth;
  let docH = Math.round(cm.doc.height + paddingVert(cm.display));
  return {
    clientHeight: d.scroller.clientHeight,
    viewHeight: d.wrapper.clientHeight,
    scrollWidth: d.scroller.scrollWidth,
    clientWidth: d.scroller.clientWidth,
    viewWidth: d.wrapper.clientWidth,
    barLeft: cm.options.fixedGutter ? gutterW : 0,
    docHeight: docH,
    scrollHeight: docH + scrollGap(cm) + d.barHeight,
    nativeBarWidth: d.nativeBarWidth,
    gutterWidth: gutterW,
  };
}

class NativeScrollbars {
  constructor(place, scroll, cm) {
    this.cm = cm;
    let vert = (this.vert = elt('div', [elt('div', null, null, 'min-width: 1px')], 'CodeMirror-vscrollbar'));
    let horiz = (this.horiz = elt(
      'div',
      [elt('div', null, null, 'height: 100%; min-height: 1px')],
      'CodeMirror-hscrollbar'
    ));
    vert.tabIndex = horiz.tabIndex = -1;
    place(vert);
    place(horiz);

    on(vert, 'scroll', () => {
      if (vert.clientHeight) scroll(vert.scrollTop, 'vertical');
    });
    on(horiz, 'scroll', () => {
      if (horiz.clientWidth) scroll(horiz.scrollLeft, 'horizontal');
    });

    this.checkedZeroWidth = false;
    // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
    if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = '18px';
  }

  update(measure) {
    let needsH = measure.scrollWidth > measure.clientWidth + 1;
    let needsV = measure.scrollHeight > measure.clientHeight + 1;
    let sWidth = measure.nativeBarWidth;

    if (needsV) {
      this.vert.style.display = 'block';
      this.vert.style.bottom = needsH ? sWidth + 'px' : '0';
      let totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
      // A bug in IE8 can cause this value to be negative, so guard it.
      this.vert.firstChild.style.height = Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + 'px';
    } else {
      this.vert.style.display = '';
      this.vert.firstChild.style.height = '0';
    }

    if (needsH) {
      this.horiz.style.display = 'block';
      this.horiz.style.right = needsV ? sWidth + 'px' : '0';
      this.horiz.style.left = measure.barLeft + 'px';
      let totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
      this.horiz.firstChild.style.width = Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + 'px';
    } else {
      this.horiz.style.display = '';
      this.horiz.firstChild.style.width = '0';
    }

    if (!this.checkedZeroWidth && measure.clientHeight > 0) {
      if (sWidth == 0) this.zeroWidthHack();
      this.checkedZeroWidth = true;
    }

    return { right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0 };
  }

  setScrollLeft(pos) {
    if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
    if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz, 'horiz');
  }

  setScrollTop(pos) {
    if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
    if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert, 'vert');
  }

  zeroWidthHack() {
    let w = mac && !mac_geMountainLion ? '12px' : '18px';
    this.horiz.style.height = this.vert.style.width = w;
    this.horiz.style.pointerEvents = this.vert.style.pointerEvents = 'none';
    this.disableHoriz = new Delayed();
    this.disableVert = new Delayed();
  }

  enableZeroWidthBar(bar, delay, type) {
    bar.style.pointerEvents = 'auto';
    function maybeDisable() {
      // To find out whether the scrollbar is still visible, we
      // check whether the element under the pixel in the bottom
      // right corner of the scrollbar box is the scrollbar box
      // itself (when the bar is still visible) or its filler child
      // (when the bar is hidden). If it is still visible, we keep
      // it enabled, if it's hidden, we disable pointer events.
      let box = bar.getBoundingClientRect();
      let elt =
        type == 'vert'
          ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2)
          : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1);
      if (elt != bar) bar.style.pointerEvents = 'none';
      else delay.set(1000, maybeDisable);
    }
    delay.set(1000, maybeDisable);
  }

  clear() {
    let parent = this.horiz.parentNode;
    parent.removeChild(this.horiz);
    parent.removeChild(this.vert);
  }
}

class NullScrollbars {
  update() {
    return { bottom: 0, right: 0 };
  }
  setScrollLeft() {}
  setScrollTop() {}
  clear() {}
}

export function updateScrollbars(cm, measure) {
  if (!measure) measure = measureForScrollbars(cm);
  let startWidth = cm.display.barWidth,
    startHeight = cm.display.barHeight;
  updateScrollbarsInner(cm, measure);
  for (let i = 0; (i < 4 && startWidth != cm.display.barWidth) || startHeight != cm.display.barHeight; i++) {
    if (startWidth != cm.display.barWidth && cm.options.lineWrapping) updateHeightsInViewport(cm);
    updateScrollbarsInner(cm, measureForScrollbars(cm));
    startWidth = cm.display.barWidth;
    startHeight = cm.display.barHeight;
  }
}

// Re-synchronize the fake scrollbars with the actual size of the
// content.
function updateScrollbarsInner(cm, measure) {
  let d = cm.display;
  let sizes = d.scrollbars.update(measure);

  d.sizer.style.paddingRight = (d.barWidth = sizes.right) + 'px';
  d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + 'px';
  d.heightForcer.style.borderBottom = sizes.bottom + 'px solid transparent';

  if (sizes.right && sizes.bottom) {
    d.scrollbarFiller.style.display = 'block';
    d.scrollbarFiller.style.height = sizes.bottom + 'px';
    d.scrollbarFiller.style.width = sizes.right + 'px';
  } else d.scrollbarFiller.style.display = '';
  if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
    d.gutterFiller.style.display = 'block';
    d.gutterFiller.style.height = sizes.bottom + 'px';
    d.gutterFiller.style.width = measure.gutterWidth + 'px';
  } else d.gutterFiller.style.display = '';
}

export let scrollbarModel = { native: NativeScrollbars, null: NullScrollbars };

export function initScrollbars(cm) {
  if (cm.display.scrollbars) {
    cm.display.scrollbars.clear();
    if (cm.display.scrollbars.addClass) rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
  }

  cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](
    node => {
      cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
      // Prevent clicks in the scrollbars from killing focus
      on(node, 'mousedown', () => {
        if (cm.state.focused) setTimeout(() => cm.display.input.focus(), 0);
      });
      node.setAttribute('cm-not-content', 'true');
    },
    (pos, axis) => {
      if (axis == 'horizontal') setScrollLeft(cm, pos);
      else updateScrollTop(cm, pos);
    },
    cm
  );
  if (cm.display.scrollbars.addClass) addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
}
